iOS 中 runtime 简单介绍

runtime 总是一副很神秘的样子,就像盖着盖头的新娘,今天就揭下盖头看看新娘(runtime)究竟神秘样子!当然即使揭下盖头也只能看看新娘(runtime)的外貌,究竟内心是什么样的人,还需要长时间的相处去发现。

什么 runtime

runtime简称运行时,是一个c和汇编写的动态库,为 C 添加了面相对象的能力并创造了 Objective-C。也是系统运行时的一些机制,其中最主要的是消息机制。

1
2
C语言:函数的调用在编译时会决定调用哪个函数
OC语言:动态调用,也就是说编译时并不能决定真正调用哪个函数,只有在运行时才会根据函数名找到相应的函数调用。

怎样使用 runtime

说了一些没用的废话,那么 runtime 是怎样来使用的呢?下面就简单介绍一下。

常用的头文件

1
2
#import <objc/runtime.h> 包含对类、成员变量、属性、方法的操作
#import <objc/message.h> 包含消息机制

发送消息

OC 中方法调用的本质就是让对象发送消息。

比如在 OC 中这样调用方法

1
2
3
4
5
6
7
8
9
10
11
12
// 实例方法
Person *p = [[Person alloc]init];
[p eat];

// 本质:让对象发送消息(在编译时会通过runtime 转换成下面的代码)
objc_msgSend(p, @selector(eat));

// 类方法
[Person eat];

// 本质:底层会把类名调用转换成对象调用,仍然是对象发送消息
objc_msgSend([Person class], @selector(eat));

消息传递的关键是编译器构建每个类和对象时采用的数据结构,每个类必须包含2个必要元素:

1.一个指向父类的指针

2.一个调度表(dispatch table)将类的selector(方法名) 与方法的实际内存地址关联起来。

每个对象都有一个指向其所属类的指针 isa 。

当对象发送消息时,objc_msgSend() 方法根据对象的 isa 指针找到对象的类,然后在类的调度表(dispatch table)中查找 selector ,如果没有找到,objc_msgSend() 方法通过指向父类的指针找到父类,然后在父类的调度表中查找 selector,以此类推直到找到 NSObject 类。一旦找到 selector ,objc_msgSend()方法根据调度表中 selector 的内存地址调用实现。

上面的方法每次都要查找整个调度表,如果类的方法很多,执行起来会消耗较多的时间。为了保证消息的发送与执行速度,系统会将使用过的 selector 会 方法的内存地址缓存起来,每个类都有一个缓存区域,包含当前类使用过的 selector 和父类中使用过的 selector。查找调度表之前,会先查找对象所属类的缓存。

当 selector 没有被实现时,会面临2中情况:

1
2
1. 使用 [p eat] 调用
2. 使用 [p performSelector:@selector(eat)]; 调用

第一种情况编译器会报错,第二种情况需要运行时才能确定对象能否接受到消息,这时会进入消息转发流程:

  • 消息转发流程

1、动态方法解析
接收到未知消息时(假设person的eat方法尚未实现),runtime会调用+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法),比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSString *s = NSStringFromSelector(sel);
if ([s isEqualToString:@"eat"]) {
class_addMethod([Person class], @selector(eat), (IMP)eatFunc, "v@:");
}

return [super resolveInstanceMethod:sel];
}

void eatFunc(id self, SEL _cmd)
{
NSLog(@"hello");
}

简单介绍一下 class_addMethod 方法的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) 
参数说明:

cls:被添加方法的类,这里不要使用object_getClass(self),而是使用 [Person class] 来获取当前类

name:未实现的方法名,比如我的eat方法未实现,这里就用eat

imp:转发的函数,函数名随便命名,比如这里就是eatFunc

types:一个定义该函数返回值类型和参数类型的字符串

这里的type为“v@:”
v :表示返回值为void ,若是 i 则表示 返回值类型为 int
@ :参数 id(self)
: : SEL(_cmd)

2、备用接收者
如果以上方法没有做处理,runtime会调用- (id)forwardingTargetForSelector:(SEL)aSelector方法。
如果该方法返回了一个非nil(也不能是self)的对象,而且该对象实现了这个方法,那么这个对象就成了消息的接收者,消息就被分发到该对象。
适用情况:通常在对象内部使用,让内部的另外一个对象处理消息,在外面看起来就像是该对象处理了消息。
比如:person 让女朋友 personGirlFriend 来接收这个消息

1
2
3
4
5
6
7
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSString * s = NSStringFromSelector(aSelector);
if ([s isEqualToString:@"eat"]) {
return self. personGirlFriend;
}
return [super forwardingTargetForSelector:aSelector];
}

3、完整消息转发
在- (void)forwardInvocation:(NSInvocation *)anInvocation方法中选择转发消息的对象,其中anInvocation对象封装了未知消息的所有细节,并保留调用结果发送到原始调用者。
比如:person将消息完整转发給父母 PersonParent来处理

1
2
3
4
5
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([PersonParent instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:self. personParent];
}
}

4、如果在以上三个方法都没有处理未知消息,则会引发异常。

遍历对象属性

OC 中成员变量的实质什么?方法的实质又是什么?类的实质又是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;

/// An opaque type that represents a category.
typedef struct objc_category *Category;

/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t;

-------------------------------------------------
从上面可以看出:
成员变量的实质是一个指向 objc_ivar 结构体的指针。
方法的实质是一个指向 objc_method 结构体的指针
属性变量的实质是一个指向 objc_property 结构体的指针

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

类的实质就是一个指向 objc_class 结构体的指针
  • 使用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 对成员变量的一些操作
- (void)getIvarList:(id)object
{
u_int count;
// class_copyIvarList 获取所有成员变量(包括property属性)
Ivar *lists = class_copyIvarList(object_getClass(object), &count);

for (int i=0; i<count; i++) {
// 成员变量
Ivar list = lists[i];
// 变量名
const char *name = ivar_getName(list);
// 变量类型
const char *type = ivar_getTypeEncoding(list);
// 获取指定名称的成员变量
Ivar sList = class_getClassVariable(object_getClass(object), "listName");
// 获取某个成员变量的值
id value = object_getIvar(object_getClass(object), sList);
// 为某个成员变量赋值
id reValue = (id)@"RENAME";
object_setIvar(object_getClass(object), sList, reValue);
}
// 需要手动释放
free(lists);
}

// 对属性列表的一些操作
- (void)getProperty:(id)object
{
u_int count;
// class_copyPropertyList 获取属性列表
objc_property_t *properties = class_copyPropertyList(object_getClass(object), &count);

for (int i=0; i<count; i++) {
// 属性
objc_property_t property = properties[i];
// 属性名
const char *name = property_getName(property);
// 属性类型
const char *type = property_getAttributes(property);
// 获取属性的修饰词
u_int outCount;
objc_property_t *types = property_copyAttributeList(property, &outCount);
}
// 需要手动释放
free(properties);
}
  • 使用实例1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// 将json 数据转换成model

// 方法一:
- (void)method1:(NSDictionary *)dic
{
// 必须类型一一对应,不然有可能报错,比如字典中name是@(1234)NSNumber类型,model中name是NSString类型,如果调用name.length 程序就会报错。
// 使用系统方法
[self setValuesForKeysWithDictionary:dic];

}

// 针对上面说的问题的解决方案

// 创建一个NSNumber 的类目

// 解决方案一:runtime(方法转移)
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSString *s = NSStringFromSelector(sel);
if ([s isEqualToString:@"length"]) {
class_addMethod([NSNumber class], @selector(length), (IMP)lengthFunc, "v@:");
}

return [super resolveInstanceMethod:sel];
}

int lengthFunc(id self, SEL _cmd)
{
NSString *s = [NSString stringWithFormat:@"%@",self];
return (int)s.length;
}

// 解决方案二:添加一个叫length的方法
- (int)length
{
NSString *s = [NSString stringWithFormat:@"%@",self];
return (int)s.length;
}


// 方法二:
- (void)method2:(NSDictionary *)dic
{
NSMutableDictionary *keys = [NSMutableDictionary dictionary];
u_int count;
objc_property_t *propertyLists = class_copyPropertyList(object_getClass(self), &count);

for (int i=0; i<count; i++) {

objc_property_t propertyList = propertyLists[i];
NSString *propertyName = [NSString stringWithCString:property_getName(propertyList) encoding:NSUTF8StringEncoding];
NSString *propertyType = [NSString stringWithCString:property_getAttributes(propertyList) encoding:NSUTF8StringEncoding];

// propertyType 的各个值的意义
//属性类型 name值:T value:变化
//编码类型 name值:C(copy) &(strong) W(weak) 空(assign) 等 value:无
//非/原子性 name值:空(atomic) N(Nonatomic) value:无
//变量名称 name值:V value:变化

// 裁剪类型字符串,不适用基本数据类型,只适用NSString,NSNumber,etc
NSRange range = [propertyType rangeOfString:@"\""];
propertyType = [propertyType substringFromIndex:range.location + range.length];
range = [propertyType rangeOfString:@"\""];
// 裁剪到哪个角标,不包括当前角标
propertyType = [propertyType substringToIndex:range.location];

[keys setObject:propertyType forKey:propertyName];

}

free(propertyLists);

for (NSString *key in [keys allKeys]) {

if ([dic objectForKey:key]) {

NSString *type = [keys objectForKey:key];
if ([type isEqualToString:@"NSString"]) {

NSString* value = [NSString stringWithFormat:@"%@",[dic objectForKey:key]];
[self setValue:value forKey:key];

}
if ([type isEqualToString:@"NSNumber"])
{
NSNumber *value = [dic objectForKey:key];
// __NSCFString,__NSCFNumber,__NSCFBoolean,__NSCFArray,__NSCFDictionary
if ([value isKindOfClass:NSClassFromString(@"__NSCFString")]) {
// @"12"
value = value;
}else if ([value isKindOfClass:NSClassFromString(@"__NSCFNumber")]){
// @(12),@(12.3),etc
value = @([value floatValue]);
}else if ([value isKindOfClass:NSClassFromString(@"__NSCFBoolean")])
{ // @(YES),@(NO)
value = @([value boolValue]);
}
[self setValue:value forKey:key];
}
}
}
}

// 方法三:找一个比较好的第三方库。
  • 使用实例2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 有时想查看model各个属性的值,但是调试时打印出来的却是一个地址,那么怎样解决这个问题呢?重写debugDescription方法。

- (NSString *)debugDescription
{
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
u_int count;
objc_property_t *properties = class_copyPropertyList([self class], &count);

for (int i = 0; i<count; i++) {
objc_property_t property = properties[i];
NSString *name = @(property_getName(property));
id value = [self valueForKey:name]?:@"nil";
[dictionary setObject:value forKey:name];
}

free(properties);

return [NSString stringWithFormat:@"<%@: %p> %@",[self class],self,dictionary];
}

添加属性与方法

该如何给系统的类(比如UIButton)添加一个属性或者方法呢?你可能想到了继承。但是如果不能用继承呢?你也许还会想到分类,但是分类只能添加方法不能添加属性啊,不要着急runtime 来了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 如何为系统的类添加属性

方案一:继承

方案二:分类+runtime(只用分类不能添加属性)

// 创建UIButton 的分类

// .h 文件
@property (nonatomic, strong) NSString *url;

// .m 文件
- (void)setUrl:(NSString *)url
{
// 关联对象
objc_setAssociatedObject(self, @selector(url), url, OBJC_ASSOCIATION_COPY);
}

- (NSString *)url{
// 获取关联对象
return objc_getAssociatedObject(self, @selector(url));
}

// 然后就可以访问buton 的 url 属性了,如果需要移除关联对象,将关联对象的值设为nil 即可。

// 当然如果只是在一个类中使用button 的 url 属性, 那么没必要建一个分类,直接在调用类中实现:

// 设置关联
objc_setAssociatedObject(button, @"link", @"这是链接测试", OBJC_ASSOCIATION_COPY);
// 获取关联的值
NSString *link = objc_getAssociatedObject(button, @"link");

// 添加方法:继承,分类,runtime(见上面的描述)

方法交换

有些时候我们希望有自己的方法取代系统的方法(按照自己的方法实现,或者防止程序崩溃),比如防止数组越界或者传入值为nil 时,系统的崩溃。

改变系统方法的实现的方式有:继承,分类(类目),这里再说一种方法的交换。具体需要那种方式自己去判断。

方法交换的相关函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 通过方法名获取方法
class_getInstanceMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>)

// 获取一个方法的实现
method_getImplementation(<#Method m#>)

// 获取一个OC实现的编码类型
method_getTypeEncoding(<#Method m#>)

// 添加一个方法
class_addMethod(__unsafe_unretained Class cls, <#SEL name#>, <#IMP imp#>, <#const char *types#>)

// 用一个方法的实现替换
class_replaceMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>, <#IMP imp#>, <#const char *types#>)

// 交换2个方法的实现
method_exchangeImplementations(<#Method m1#>, <#Method m2#>)
  • 使用实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 防止数组越界
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

Method method1 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method method2 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(z_objectAtIndex:));
method_exchangeImplementations(method1, method2);
});
}

- (id)z_objectAtIndex:(NSUInteger)index
{
NSLog(@"~~~~~~~:%ld",count);
if (index >= self.count) {
return @"haha";
}else
{
return [self z_objectAtIndex:index];
}
}

// 上面的写法能解决数组越界的问题,但是z_objectAtIndex: 方法会调用很多次,而且个一段时间就会调用几次,不清楚为什么。是就是这样的机制,还是写法有误?


// 替换imageNamed 方法
+ (void) load
{
Method method1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
Method method2 = class_getClassMethod([UIImage class], @selector(z_imageNamed:));

method_exchangeImplementations(method1, method2);
// IMP imp1 = method_getImplementation(method1);
// IMP imp2 = method_getImplementation(method2);
// method_setImplementation(method1, imp2);
// method_setImplementation(method2, imp1);
}

+ (UIImage *)z_imageNamed:(NSString *)name
{
NSLog(@"修改名字");
return [self z_imageNamed:@"name"];
}
// 这里的z_imageNamed:就只会调用一次。

如何防止button的连点

总结

经过上面的学习,只能说对 runtime 有了一些了解,能够简单的使用一下,如果想要更深入的了解 runtime 还需要持续不断的深入学习。

参考博客:

Objective-C对象模型及应用

objc/runtime 探索

iOS开发教程之Objc Runtime笔记

如果觉得写的不错,那就打赏一下吧!